﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace SBS_Map_Editor
{
    #region Enumerator
    public enum TransformSpace
    {
        CameraSpace,
        WorldSpace,
    }
    #endregion

    public sealed class QuaternionCamera : ICamera
    {
        #region Variables
        private Matrix mViewMatrix;
        private Matrix mInverseViewMatrix;
        private Matrix mProjectionMatrix;

        private float mFieldOfView;
        private float mAspectRatio;
        private float mNearPlaneDistance;
        private float mFarPlaneDistance;

        private Vector3 mPosition;
        private Quaternion mOrientation;

        private bool mBuildView;
        private bool mBuildProjection;
        #endregion

        #region Properties
        public Matrix ViewMatrix
        {
            get
            {
                if (mBuildView)
                    BuildViewMatrix();

                return mViewMatrix;
            }
        }

        public Matrix InverseViewMatrix
        {
            get
            {
                if (mBuildView)
                    BuildViewMatrix();

                return mInverseViewMatrix;
            }
        }

        public Matrix ProjectionMatrix
        {
            get
            {
                if (mBuildProjection)
                    BuildProjectionMatrix();

                return mProjectionMatrix;
            }
        }

        public float FieldOfView
        {
            get { return mFieldOfView; }
            set
            {
                mFieldOfView = value;
                mBuildProjection = true;
            }
        }

        public float AspectRatio
        {
            get { return mAspectRatio; }
            set
            {
                mAspectRatio = value;
                mBuildProjection = true;
            }
        }

        public float NearPlaneDistance
        {
            get { return mNearPlaneDistance; }
            set
            {
                mNearPlaneDistance = value;
                mBuildProjection = true;
            }
        }

        public float FarPlaneDistance
        {
            get { return mFarPlaneDistance; }
            set
            {
                mFarPlaneDistance = value;
                mBuildProjection = true;
            }
        }

        public Vector3 Position
        {
            get { return mPosition; }
            set
            {
                mPosition = value;
                mBuildView = true;
            }
        }

        public Quaternion Orientation
        {
            get { return mOrientation; }
            set
            {
                Quaternion.Normalize(ref value, out mOrientation);
                mBuildView = true;
            }
        }

        public float X
        {
            get { return mPosition.X; }
            set
            {
                mPosition.X = value;
                mBuildView = true;
            }
        }

        public float Y
        {
            get { return mPosition.Y; }
            set
            {
                mPosition.Y = value;
                mBuildView = true;
            }
        }

        public float Z
        {
            get { return mPosition.Z; }
            set
            {
                mPosition.Z = value;
                mBuildView = true;
            }
        }

        public Vector3 Up
        {
            get
            {
                if (mBuildView)
                    BuildViewMatrix();

                return mInverseViewMatrix.Up;
            }
        }

        public Vector3 Forward
        {
            get
            {
                if (mBuildView)
                    BuildViewMatrix();

                return mInverseViewMatrix.Forward;
            }
        }

        public Vector3 Right
        {
            get
            {
                if (mBuildView)
                    BuildViewMatrix();

                return mInverseViewMatrix.Right;
            }
        }

        public BoundingFrustum Frustum
        {
            get { return new BoundingFrustum(ViewMatrix * ProjectionMatrix); }
        }
        #endregion

        #region Constructor
        public QuaternionCamera()
            : this(Vector3.Zero, Quaternion.Identity, (float)Math.PI * 0.25f, 4f / 3f, 1f, 1000f)
        {
        }

        public QuaternionCamera(Vector3 position, Quaternion orientation)
            : this(position, orientation, (float)Math.PI * 0.25f, 4f / 3f, 1f, 1000f)
        {
        }

        public QuaternionCamera(Vector3 position, Quaternion orientation, float fov, float aspect, float near, float far)
        {
            mPosition = position;
            mOrientation = orientation;
            mViewMatrix = Matrix.Identity;
            mInverseViewMatrix = Matrix.Identity;
            mProjectionMatrix = Matrix.Identity;

            mFieldOfView = fov;
            mAspectRatio = aspect;
            mNearPlaneDistance = near;
            mFarPlaneDistance = far;

            mBuildView = true;
            mBuildProjection = true;
        }
        #endregion

        #region ActionFunctions
        public void Update(float elapsedSeconds)
        {
            if (mBuildView)
                BuildViewMatrix();

            if (mBuildProjection)
                BuildProjectionMatrix();
        }
        #endregion

        #region Helper Functions
        #region 3D Movement
        #region Pitch
        public void Pitch(float pitchAmount, TransformSpace space)
        {
            if (mBuildView)
                BuildViewMatrix();

            switch (space)
            {
                case TransformSpace.CameraSpace:
                    mOrientation *= Quaternion.CreateFromAxisAngle(Vector3.Normalize(mInverseViewMatrix.Right), pitchAmount);
                    break;

                case TransformSpace.WorldSpace:
                    mOrientation *= Quaternion.CreateFromAxisAngle(Vector3.Right, pitchAmount);
                    break;
            }

            mBuildView = true;
        }

        public void Pitch(float pitchAmount)
        {
            Pitch(pitchAmount, TransformSpace.CameraSpace);
        }
        #endregion

        #region Yaw
        public void Yaw(float yawAmount, TransformSpace space)
        {
            if (mBuildView)
                BuildViewMatrix();

            switch (space)
            {
                case TransformSpace.CameraSpace:
                    mOrientation *= Quaternion.CreateFromAxisAngle(Vector3.Normalize(mInverseViewMatrix.Up), yawAmount);
                    break;

                case TransformSpace.WorldSpace:
                    mOrientation *= Quaternion.CreateFromAxisAngle(Vector3.Up, yawAmount);
                    break;
            }

            mBuildView = true;
        }

        public void Yaw(float yawAmount)
        {
            Yaw(yawAmount, TransformSpace.CameraSpace);
        }
        #endregion

        #region Roll
        public void Roll(float rollAmount, TransformSpace space)
        {
            if (mBuildView)
                BuildViewMatrix();

            switch (space)
            {
                case TransformSpace.CameraSpace:
                    mOrientation *= Quaternion.CreateFromAxisAngle(Vector3.Normalize(mInverseViewMatrix.Backward), rollAmount);
                    break;

                case TransformSpace.WorldSpace:
                    mOrientation *= Quaternion.CreateFromAxisAngle(Vector3.Backward, rollAmount);
                    break;
            }

            mBuildView = true;
        }

        public void Roll(float rollAmount)
        {
            Roll(rollAmount, TransformSpace.CameraSpace);
        }
        #endregion

        #region Translate
        public void Translate(Vector3 amount, TransformSpace space)
        {
            if (mBuildView)
                BuildViewMatrix();

            switch (space)
            {
                case TransformSpace.CameraSpace:
                    mPosition += mInverseViewMatrix.Right * amount.X +
                        mInverseViewMatrix.Up * amount.Y + mInverseViewMatrix.Backward * amount.Z;
                    break;

                case TransformSpace.WorldSpace:
                    mPosition += amount;
                    break;
            }

            mBuildView = true;
        }

        public void Translate(Vector3 amount)
        {
            Translate(amount, TransformSpace.CameraSpace);
        }

        public void TranslateX(float amount, TransformSpace space)
        {
            if (mBuildView)
                BuildViewMatrix();

            switch (space)
            {
                case TransformSpace.CameraSpace:
                    mPosition += mInverseViewMatrix.Right * amount;
                    break;

                case TransformSpace.WorldSpace:
                    mPosition.X += amount;
                    break;
            }

            mBuildView = true;
        }

        public void TranslateX(float amount)
        {
            TranslateX(amount, TransformSpace.CameraSpace);
        }

        public void TranslateY(float amount, TransformSpace space)
        {
            if (mBuildView)
                BuildViewMatrix();

            switch (space)
            {
                case TransformSpace.CameraSpace:
                    mPosition += mInverseViewMatrix.Up * amount;
                    break;

                case TransformSpace.WorldSpace:
                    mPosition.Y += amount;
                    break;
            }

            mBuildView = true;
        }

        public void TranslateY(float amount)
        {
            TranslateY(amount, TransformSpace.CameraSpace);
        }

        public void TranslateZ(float amount, TransformSpace space)
        {
            if (mBuildView)
                BuildViewMatrix();

            switch (space)
            {
                case TransformSpace.CameraSpace:
                    mPosition += mInverseViewMatrix.Backward * amount;
                    break;

                case TransformSpace.WorldSpace:
                    mPosition.Z += amount;
                    break;
            }

            mBuildView = true;
        }

        public void TranslateZ(float amount)
        {
            TranslateZ(amount, TransformSpace.CameraSpace);
        }
        #endregion
        #endregion

        #region Build Matrix
        private void BuildViewMatrix()
        {
            Vector3 P;
            Quaternion O;
            Matrix T;
            Matrix R;

            Vector3.Negate(ref mPosition, out P);
            Quaternion.Negate(ref mOrientation, out O);

            Matrix.CreateTranslation(ref P, out T);
            Matrix.CreateFromQuaternion(ref O, out R);

            Matrix.Multiply(ref T, ref R, out mViewMatrix);
            Matrix.Invert(ref mViewMatrix, out mInverseViewMatrix);

            mBuildView = false;
        }

        private void BuildProjectionMatrix()
        {
            mProjectionMatrix = Matrix.CreatePerspectiveFieldOfView(mFieldOfView, mAspectRatio, mNearPlaneDistance, mFarPlaneDistance);

            mBuildProjection = false;
        }
        #endregion

        #region Ray
        public Ray GetPickRay(Vector2 position, Viewport viewport)
        {
            Vector3 near = viewport.Unproject(new Vector3(position, 0f), ProjectionMatrix, ViewMatrix, Matrix.Identity);
            Vector3 far = viewport.Unproject(new Vector3(position, 0f), ProjectionMatrix, ViewMatrix, Matrix.Identity);

            return new Ray(near, Vector3.Normalize(far - near));
        }
        #endregion
        #endregion
    }
}